import Immutable from 'immutable';
import React from 'react';
import PropTypes from 'prop-types';
import ViewportMercator from 'viewport-mercator-project';
import { roundDecimal } from '../../modules/utils';
import { kmToPixels, MILES_PER_KM } from '../../modules/geo';
import { rgbLuminance } from '../../utils/common';

const propTypes = {
  aggregation: PropTypes.string,
  locations: PropTypes.instanceOf(Immutable.List).isRequired,
  lngLatAccessor: PropTypes.func,
  renderWhileDragging: PropTypes.bool,
  globalOpacity: PropTypes.number,
  dotRadius: PropTypes.number,
  dotFill: PropTypes.string,
  compositeOperation: PropTypes.string,
};

const defaultProps = {
  lngLatAccessor: location => [location.get(0), location.get(1)],
  renderWhileDragging: true,
  dotRadius: 4,
  dotFill: '#1FBAD6',
  globalOpacity: 1,
  // Same as browser default.
  compositeOperation: 'source-over',
};

const contextTypes = {
  viewport: PropTypes.object,
  isDragging: PropTypes.bool,
};

const computeClusterLabel = (properties, aggregation) => {
  const count = properties.get('point_count');
  if (!aggregation) {
    return count;
  }
  if (aggregation === 'sum' || aggregation === 'min' || aggregation === 'max') {
    return properties.get(aggregation);
  }
  const sum = properties.get('sum');
  const mean = sum / count;
  if (aggregation === 'mean') {
    return Math.round(100 * mean) / 100;
  }
    const squaredSum = properties.get('squaredSum');
    const variance = (squaredSum / count) - Math.pow(sum / count, 2);
    if (aggregation === 'var') {
      return Math.round(100 * variance) / 100;
    } else if (aggregation === 'stdev') {
      return Math.round(100 * Math.sqrt(variance)) / 100;
    }
    // fallback to point_count, this really shouldn't happen
    return count;
};

class ScatterPlotGlowOverlay extends React.Component {
  constructor(props) {
    super(props);
    this.setCanvasRef = this.setCanvasRef.bind(this);
  }

  componentDidMount() {
    this.redraw();
  }

  componentDidUpdate() {
    this.redraw();
  }

  setCanvasRef(element) {
    this.canvas = element;
  }

  drawText(ctx, pixel, options = {}) {
    const IS_DARK_THRESHOLD = 110;
    const { fontHeight = 0, label = '', radius = 0, rgb = [0, 0, 0], shadow = false } = options;
    const maxWidth = radius * 1.8;
    const luminance = rgbLuminance(rgb[1], rgb[2], rgb[3]);

    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
    ctx.font = fontHeight + 'px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    if (shadow) {
      ctx.shadowBlur = 15;
      ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
    }

    const textWidth = ctx.measureText(label).width;
    if (textWidth > maxWidth) {
      const scale = fontHeight / textWidth;
      ctx.font = scale * maxWidth + 'px sans-serif';
    }

    ctx.fillText(label, pixel[0], pixel[1]);
    ctx.globalCompositeOperation = this.props.compositeOperation;
    ctx.shadowBlur = 0;
    ctx.shadowColor = '';
  }

  // Modified: https://github.com/uber/react-map-gl/blob/master/overlays/scatterplot.react.js
  redraw() {
    const props = this.props;
    const pixelRatio = window.devicePixelRatio || 1;
    const ctx = this.canvas.getContext('2d');
    const radius = props.dotRadius;
    const mercator = new ViewportMercator(props);
    const rgb = props.rgb;
    const clusterLabelMap = [];

    props.locations.forEach(function (location, i) {
      if (location.get('properties').get('cluster')) {
        clusterLabelMap[i] = computeClusterLabel(location.get('properties'),
            props.aggregation);
      }
    }, this);
    const maxLabel = Math.max(...Object.values(clusterLabelMap));

    ctx.save();
    ctx.scale(pixelRatio, pixelRatio);
    ctx.clearRect(0, 0, props.width, props.height);
    ctx.globalCompositeOperation = props.compositeOperation;

    if ((props.renderWhileDragging || !props.isDragging) && props.locations) {
      props.locations.forEach(function _forEach(location, i) {
        const pixel = mercator.project(props.lngLatAccessor(location));
        const pixelRounded = [roundDecimal(pixel[0], 1), roundDecimal(pixel[1], 1)];

        if (pixelRounded[0] + radius >= 0
              && pixelRounded[0] - radius < props.width
              && pixelRounded[1] + radius >= 0
              && pixelRounded[1] - radius < props.height) {
          ctx.beginPath();
          if (location.get('properties').get('cluster')) {
            let clusterLabel = clusterLabelMap[i];
            const scaledRadius = roundDecimal(Math.pow(clusterLabel / maxLabel, 0.5) * radius, 1);
            const fontHeight = roundDecimal(scaledRadius * 0.5, 1);
            const gradient = ctx.createRadialGradient(
              pixelRounded[0], pixelRounded[1], scaledRadius,
              pixelRounded[0], pixelRounded[1], 0,
            );

            gradient.addColorStop(1, 'rgba(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ', 0.8)');
            gradient.addColorStop(0, 'rgba(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ', 0)');
            ctx.arc(pixelRounded[0], pixelRounded[1], scaledRadius, 0, Math.PI * 2);
            ctx.fillStyle = gradient;
            ctx.fill();

            if (Number.isFinite(parseFloat(clusterLabel))) {
              if (clusterLabel >= 10000) {
                clusterLabel = Math.round(clusterLabel / 1000) + 'k';
              } else if (clusterLabel >= 1000) {
                clusterLabel = (Math.round(clusterLabel / 100) / 10) + 'k';
              }
              this.drawText(ctx, pixelRounded, {
                fontHeight,
                label: clusterLabel,
                radius: scaledRadius,
                rgb,
                shadow: true,
              });
            }
          } else {
            const defaultRadius = radius / 6;
            const radiusProperty = location.get('properties').get('radius');
            const pointMetric = location.get('properties').get('metric');
            let pointRadius = radiusProperty === null ? defaultRadius : radiusProperty;
            let pointLabel;

            if (radiusProperty !== null) {
              const pointLatitude = props.lngLatAccessor(location)[1];
              if (props.pointRadiusUnit === 'Kilometers') {
                pointLabel = roundDecimal(pointRadius, 2) + 'km';
                pointRadius = kmToPixels(pointRadius, pointLatitude, props.zoom);
              } else if (props.pointRadiusUnit === 'Miles') {
                pointLabel = roundDecimal(pointRadius, 2) + 'mi';
                pointRadius = kmToPixels(pointRadius * MILES_PER_KM, pointLatitude, props.zoom);
              }
            }

            if (pointMetric !== null) {
              pointLabel = Number.isFinite(parseFloat(pointMetric))
                ? roundDecimal(pointMetric, 2)
                : pointMetric;
            }

            // Fall back to default points if pointRadius wasn't a numerical column
            if (!pointRadius) {
              pointRadius = defaultRadius;
            }

            ctx.arc(pixelRounded[0], pixelRounded[1], roundDecimal(pointRadius, 1), 0, Math.PI * 2);
            ctx.fillStyle = 'rgb(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ')';
            ctx.fill();

            if (pointLabel !== undefined) {
              this.drawText(ctx, pixelRounded, {
                fontHeight: roundDecimal(pointRadius, 1),
                label: pointLabel,
                radius: pointRadius,
                rgb,
                shadow: false,
              });
            }
          }
        }
      }, this);
    }
    ctx.restore();
  }

  render() {
    let width = 0;
    let height = 0;
    if (this.context.viewport) {
      width = this.context.viewport.width;
      height = this.context.viewport.height;
    }
    const { globalOpacity } = this.props;
    const pixelRatio = window.devicePixelRatio || 1;
    return (
      <canvas
        ref={this.setCanvasRef}
        width={width * pixelRatio}
        height={height * pixelRatio}
        style={{
          width: `${width}px`,
          height: `${height}px`,
          position: 'absolute',
          pointerEvents: 'none',
          opacity: globalOpacity,
          left: 0,
          top: 0,
        }}
      />
    );
  }
}

ScatterPlotGlowOverlay.propTypes = propTypes;
ScatterPlotGlowOverlay.defaultProps = defaultProps;
ScatterPlotGlowOverlay.contextTypes = contextTypes;

export default ScatterPlotGlowOverlay;
